/**
 * Copyright 2010 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package saig.engine.models;

import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;

import saig.engine.Context;
import saig.engine.Level;
import saig.engine.models.enums.ModelType;
import saig.engine.models.enums.TileType;


/**
 * Core Model Class, all other models should extend this object.
 * @author Adam
 * @version $Id$
 * 
 * Created on Jun 3, 2010 at 11:37:32 AM 
 */
public abstract class Model {
	/**
	 * The air resistance constant.
	 */
	public static final float AIR_RESISTANCE = .90f;
	/**
	 * The friction constant.
	 */
	public static final float FRICTION = .90f;
	/**
	 * The gravity constant.
	 */
	public static final float GRAVITY = 2.0f;
	/**
	 * Serial Version UID.
	 */
	private static final long serialVersionUID = -8101139538979305022L;
	/**
	 * Stores the bounding box for collision detection.
	 */
	protected final Rectangle boundingBox;
	/**
	 * The game context.
	 */
	protected final Context context;
	/**
	 * if -1 then direction is facing left, if 1 then direction is right.
	 */
	protected int direction;
	/**
	 * The amount of change (delta) of x to move.
	 */
	protected float dx;
	/**
	 * The amount of change (delta) of y to move.
	 */
	protected float dy;
	/**
	 * The current frame.
	 */
	protected int frame;
	/**
	 * An array of all the sprite frames for this model.
	 */
	protected Sprite[] frames;
	/**
	 * If the model is on the ground.
	 */
	protected boolean onGround;
	/**
	 * Stores the spawn level information if this model was created from spawning.
	 */
	protected Spawn spawn;
	/**
	 * The model type.
	 */
	protected ModelType type;
	/**
	 * The x position of the model in world coordinates.
	 */
	protected float x;
	/**
	 * The y position of the model in world coordinates.
	 */
	protected float y;
	/**
	 * Constructs a model.
	 */
	public Model(final Context context, final ModelType type) {
		this.context = context;
		this.type = type;
		this.x = 0;
		this.y = 0;
		this.frame = 0;
		this.dx = 0.0f;
		this.dy = 0.0f;
		this.direction = 1;
		this.onGround = false;
		this.boundingBox = new Rectangle();
	}
	/**
	 * Checks collisions along a horizontal line against a level.
	 * @param x The x position to test.
	 * @param y The y position to test.
	 * @param down if we are checking in the down direction (used for platform tiles).
	 * @return null if no collision otherwise a Point containing the tile position.
	 */
	protected Point checkHorizontalCollisions(final int x, final int y, final boolean down) {
		final Level level = context.getLevel();
		// The x position (world coordinates) of the tile (left).
		int tileXPos = x - (x % level.getTileWidth());
		// The ending x (world coordinates) position.
		final int endX = x + (getWidth() / 2) - 1;
		// The y position (tile coordinates) of the tile (top).
		final int tileY = y / level.getTileHeight();
		// The x position (tile coordinates) of the tile in the tile map.
		int tileX = tileXPos / level.getTileWidth();
		if (down) {
			while (tileXPos <= endX) {
				// Get the tile from the level.
				final Tile tile = level.getTile(tileX, tileY);
				// Is it an impassable or platform tile.
				if (tile != null && tile.getType() != TileType.PASSABLE && tile.getType() != TileType.HIDDEN
						&& tile.getType() != TileType.COIN) {
					return new Point(tileX, tileY);
				}
				tileXPos += level.getTileWidth();
				tileX++;
			}
		} else {
			while (tileXPos <= endX) {
				// Get the tile from the level.
				final Tile tile = level.getTile(tileX, tileY);
				// Is it an impassable or hidden tile.
				if (tile != null && tile.getType() != TileType.PASSABLE && tile.getType() != TileType.PLATFORM
						&& tile.getType() != TileType.COIN) {
					Point p = new Point(tileX, tileY);
					context.tileCollisionOccured(type, p);
					return p;
				}
				tileXPos += level.getTileWidth();
				tileX++;
			}
		}
		return null;
	}
	/**
	 * Checks to see if the player is on the ground.
	 * @param x The x position.
	 * @param y The y position.
	 * @return True if on the ground otherwise false.
	 */
	protected boolean checkOnGround() {
		final int yVal = (int) y + 1;
		final Level level = context.getLevel();
		// The x position (world coordinates) of the tile (left).
		int tileXPos = (int) x - ((int) x % level.getTileWidth());
		// The ending x (world coordinates) position.
		final int endX = (int) x + (getWidth() / 2) - 1;
		// The y position (tile coordinates) of the tile (top).
		final int tileY = yVal / level.getTileHeight();
		// The x position (tile coordinates) of the tile in the tile map.
		int tileX = tileXPos / level.getTileWidth();
		while (tileXPos <= endX) {
			// Get the tile from the level.
			final Tile tile = level.getTile(tileX, tileY);
			// If it is an impassable tile.
			if (tile != null && (tile.getType() == TileType.IMPASSABLE || tile.getType() == TileType.BREAKABLE
					|| tile.getType() == TileType.PLATFORM || tile.getType() == TileType.BUMPABLE)) {
				return true;
			}
			tileXPos += level.getTileWidth();
			tileX++;
		}
		return false;
	}
	/**
	 * Checks for collisions with the against the level and fixes the dx, dy values.
	 */
	protected void checkTileCollision() {
		final int halfWidth = getWidth() / 2;
		if (dx > 0) { // RIGHT
			int testX = (int) (x + dx + halfWidth);
			Point p = checkVerticleCollisions(testX, (int) y);
			if (p != null) {
				collisionOccurredRight(p);
			}
		} else if (dx < 0){ // LEFT
			int testX = (int) (x + dx - (halfWidth + 1));
			Point p = checkVerticleCollisions(testX, (int) y);
			if (p != null) {
				collisionOccurredLeft(p);
			}
		}
		if (dy < 0) { // UP
			int testY = (int) (y + dy - getHeight());
			Point p = checkHorizontalCollisions((int) x, testY, false);
			if (p != null) {
				collisionOccurredTop(p);
			}
		} else if (dy > 0){ // DOWN
			int textY = (int) (y + dy);
			Point p = checkHorizontalCollisions((int) x, textY, true);
			if (p != null) {
				collisionOccurredBottom(p);
			}
		}
	}
	/**
	 * Checks for collisions along a vertical line against a level.
	 * @param x The x position to test.
	 * @param y The y position to test.
	 * @return null if no collision otherwise a Point containing the tile position.
	 */
	protected Point checkVerticleCollisions(final int x, final int y) {
		final Level level = context.getLevel();
		// The y value of the top position of the player in world coordinates.
		final int top = y - getHeight() + 1;
		// The y position (world coordinates) of the tile (top).
		int tileYPos = top - (top % level.getTileHeight());
		// The x position (tile coordinates) of the tile in the tile map.
		final int tileX = x / level.getTileWidth();
		// The y position (tile coordinates) of the tile in the tile map.
		int tileY = tileYPos / level.getTileHeight();
		while (tileYPos <= y) {
			// Get the tile from the level.
			final Tile tile = level.getTile(tileX, tileY);
			// If it is an impassable tile.
			if (tile != null && (tile.getType() == TileType.IMPASSABLE || tile.getType() == TileType.BREAKABLE
					|| tile.getType() == TileType.BUMPABLE)) {
				return new Point(tileX, tileY);
			}
			tileYPos += level.getTileHeight();
			tileY++;
		}
		return null;
	}
	/**
	 * Is called from checkTileCollision if a collision along the bottom of the model occurs.
	 * @param p A point containing the tileX and tileY of the tile that the model collided with.
	 */
	protected void collisionOccurredBottom(final Point p) {
		y = (float) (p.getY() * context.getLevel().getTileHeight()) - 1f;
		dy = 0f;
	}
	/**
	 * Is called from checkTileCollision if a collision along the left side of the model occurs.
	 * @param p A point containing the tileX and tileY of the tile that the model collided with.
	 */
	protected void collisionOccurredLeft(final Point p) {
		x = (float) ((p.getX() + 1f) * context.getLevel().getTileWidth() + ((float) getWidth() / 2f)) + 1f;
		dx = 0f;
	}
	/**
	 * Is called from checkTileCollision if a collision along the right side of the model occurs.
	 * @param p A point containing the tileX and tileY of the tile that the model collided with.
	 */
	protected void collisionOccurredRight(final Point p) {
		x = (float) (p.getX() * context.getLevel().getTileWidth()) - ((float) getWidth() / 2f);
		dx = 0f;
	}
	/**
	 * Is called from checkTileCollision if a collision along the top of the model occurs.
	 * @param p A point containing the tileX and tileY of the tile that the model collided with.
	 */
	protected void collisionOccurredTop(final Point p) {
		y = (float) ((p.getY() + 1f) * context.getLevel().getTileHeight() + getHeight() - 1);
		dy = 0f;
	}
	/**
	 * Draws the model object to the graphics buffer.
	 * @param g The graphics buffer.
	 * @param cameraX The camera's x position (upper left).
	 * @param cameraY The camera's y position (upper left).
	 */
	public void doRender(final Graphics g, final float cameraX, final float cameraY) {
		final Sprite sprite = frames[frame];
		final int xPos = ((int) (x - cameraX)) - (sprite.getWidth() / 2);
		final int yPos = ((int) (y - cameraY)) - sprite.getHeight() + 1;
		if (direction < 0) {
			g.drawImage(sprite.getImage(), xPos + sprite.getWidth(), yPos,
					-sprite.getWidth(), sprite.getHeight(), null);
		} else {
			g.drawImage(sprite.getImage(), xPos, yPos, null);
		}
	}
	/**
	 * Returns the bounding box for the model for collision detection.
	 * @return The bounding box for the model for collision detection.
	 */
	public Rectangle getBoundingBox() {
		boundingBox.setBounds((int) x - (getWidth() / 2), (int ) y - getHeight() + 1, getWidth(), getHeight());
		return boundingBox;
	}
	/**
	 * Returns direction.
	 * @return the direction.
	 */
	public int getDirection() {
		return direction;
	}
	/**
	 * Returns dx.
	 * @return the dx.
	 */
	public float getDx() {
		return dx;
	}
	/**
	 * Returns dy.
	 * @return the dy.
	 */
	public float getDy() {
		return dy;
	}
	/**
	 * Returns frame.
	 * @return the frame.
	 */
	public int getFrame() {
		return frame;
	}
	/**
	 * Returns frames.
	 * @return the frames.
	 */
	public Sprite[] getFrames() {
		return frames;
	}
	/**
	 * Returns the height of the model.
	 * @return The height of the model.
	 */
	public abstract int getHeight();
	/**
	 * Returns spawn.
	 * @return the spawn.
	 */
	public Spawn getSpawn() {
		return spawn;
	}
	/**
	 * Returns type.
	 * @return the type.
	 */
	public ModelType getType() {
		return type;
	}
	/**
	 * Returns the width of the model.
	 * @return The width of the model.
	 */
	public abstract int getWidth();
	/**
	 * Returns x.
	 * @return the x.
	 */
	public float getX() {
		return x;
	}
	/**
	 * Returns y.
	 * @return the y.
	 */
	public float getY() {
		return y;
	}
	/**
	 * Returns onGround.
	 * @return the onGround.
	 */
	public boolean isOnGround() {
		return onGround;
	}
	/**
	 * Moves the model object based on it's current dx and dy values.
	 */
	public void move() {
		x += dx;
		y += dy;
	}
	/**
	 * This will be called by the game engine.  Each model must implement their update logic here.
	 */
	public abstract void performUpdate();
	/**
	 * Sets direction.
	 * @param direction the direction to set.
	 */
	public void setDirection(int direction) {
		this.direction = direction;
	}
	/**
	 * Sets dx.
	 * @param dx the dx to set.
	 */
	public void setDx(final float dx) {
		this.dx = dx;
	}
	/**
	 * Sets dy.
	 * @param dy the dy to set.
	 */
	public void setDy(final float dy) {
		this.dy = dy;
	}
	/**
	 * Sets frame.
	 * @param frame the frame to set.
	 */
	public void setFrame(final int frame) {
		this.frame = frame;
	}
	/**
	 * Sets frames.
	 * @param frames the frames to set.
	 */
	public void setFrames(final Sprite[] frames) {
		this.frames = frames;
	}
	/**
	 * Sets onGround.
	 * @param onGround the onGround to set.
	 */
	public void setOnGround(boolean onGround) {
		this.onGround = onGround;
	}
	/**
	 * Sets spawn.
	 * @param spawn the spawn to set.
	 */
	public void setSpawn(Spawn spawn) {
		this.spawn = spawn;
	}
	/**
	 * Sets x.
	 * @param x the x to set.
	 */
	public void setX(final float x) {
		this.x = x;
	}
	/**
	 * Sets y.
	 * @param y the y to set.
	 */
	public void setY(final float y) {
		this.y = y;
	}
}
